package com.indextank.roo.addon;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.Properties;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.addon.entity.RooEntity;
import org.springframework.roo.classpath.PhysicalTypeDetails;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.PhysicalTypeMetadataProvider;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.MemberFindingUtils;
import org.springframework.roo.classpath.details.MutableClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.process.manager.MutableFile;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.support.util.Assert;
import org.springframework.roo.support.util.FileCopyUtils;
import org.springframework.roo.support.util.TemplateUtils;
import org.springframework.roo.support.util.XmlElementBuilder;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


/**
 * Implementation of {@link IndextankOperations} interface.
 */
@Component
@Service
public class IndextankOperationsImpl implements IndextankOperations {
    private static final char SEPARATOR = File.separatorChar;

    private static final Dependency INDEXTANK = new Dependency("com.indextank", "indextank-java", "1.0.8");

    @Reference private FileManager fileManager;
    @Reference private MetadataService metadataService;
    @Reference private PhysicalTypeMetadataProvider physicalTypeMetadataProvider;
    @Reference private ProjectOperations projectOperations;
    @Reference private TypeLocationService typeLocationService;


    /** {@inheritDoc} */
    public boolean isInstallSearchAvailable() {
        return projectOperations.isProjectAvailable() && !indextankPropsInstalled() && fileManager.exists(projectOperations.getProjectMetadata().getPathResolver().getIdentifier(Path.SRC_MAIN_RESOURCES, "META-INF" + SEPARATOR + "persistence.xml"));
    }

    /** {@inheritDoc} */
    public boolean isSearchAvailable() {
        return indextankPropsInstalled();
    }


    /**
     * Checks whether indextank.properties file is installed.
     */ 
    private boolean indextankPropsInstalled() {
        return fileManager.exists(projectOperations.getPathResolver().getIdentifier(Path.SRC_MAIN_RESOURCES,  "META-INF" + SEPARATOR + "spring" + SEPARATOR + "indextank.properties"));
    }


    /** {@inheritDoc} */
    public void setupConfig(String apiUrl, String indexName) {

        projectOperations.addDependency(INDEXTANK);
        updateIndextankProperties(apiUrl, indexName);


        String contextPath = projectOperations.getPathResolver().getIdentifier(Path.SPRING_CONFIG_ROOT, "applicationContext.xml");
        Document appCtx = XmlUtils.readXml(fileManager.getInputStream(contextPath));
        Element root = appCtx.getDocumentElement();

        if (XmlUtils.findFirstElementByName("task:annotation-driven", root) == null) {
            if (root.getAttribute("xmlns:task").length() == 0) {
                root.setAttribute("xmlns:task", "http://www.springframework.org/schema/task");
                root.setAttribute("xsi:schemaLocation", root.getAttribute("xsi:schemaLocation") + "  http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd");
            }
            root.appendChild(new XmlElementBuilder("task:annotation-driven", appCtx).addAttribute("executor", "asyncExecutor").addAttribute("mode", "aspectj").build());
            root.appendChild(new XmlElementBuilder("task:executor", appCtx).addAttribute("id", "asyncExecutor").addAttribute("pool-size", "${executor.poolSize}").build());
        }
        Element indextankIndexClient = XmlUtils.findFirstElement("/beans/bean[@id='indextankIndexClient']", root);
        if (indextankIndexClient != null) {
            return;
        }
        root.appendChild(new XmlElementBuilder("bean", appCtx).addAttribute("id", "indextankApiClient").addAttribute("class", "com.flaptor.indextank.apiclient.IndexTankClient").addChild(new XmlElementBuilder("constructor-arg", appCtx).addAttribute("value", "${indextank.apiUrl}").build()).build());


        root.appendChild(new XmlElementBuilder("bean", appCtx).addAttribute("id", "indextankIndexClient").addAttribute("class", "org.springframework.beans.factory.config.MethodInvokingFactoryBean")
        .addChild( new XmlElementBuilder("property", appCtx).addAttribute("name", "targetObject").addChild(
                        new XmlElementBuilder("ref", appCtx).addAttribute("local", "indextankApiClient").build()
                 ).build()
        )
        .addChild( new XmlElementBuilder("property", appCtx).addAttribute("name","targetMethod").addAttribute("value","getIndex").build())
        .addChild( new XmlElementBuilder("property", appCtx).addAttribute("name","arguments").addAttribute("value", "${indextank.indexName}").build())
        .build());

        XmlUtils.removeTextNodes(root);
        fileManager.createOrUpdateTextFileIfRequired(contextPath, XmlUtils.nodeToString(appCtx), false);

    }


    private void updateIndextankProperties(String apiUrl, String indexName) {
        Assert.notNull(apiUrl, "apiUrl cannot be null");
        Assert.notNull(indexName, "indexName cannot be null");

        if (apiUrl.isEmpty()) {
            throw new IllegalArgumentException("apiUrl cannot be empty");
        }
        
        if (indexName.isEmpty()) {
            throw new IllegalArgumentException("indexName cannot be empty");
        }

        String indextankPath = projectOperations.getPathResolver().getIdentifier(Path.SPRING_CONFIG_ROOT, "indextank.properties");
        boolean indextankExists = fileManager.exists(indextankPath);
        Properties props = new Properties();
        props.put("executor.poolSize", "10");

        // try to load previous config values
        try {
            if (indextankExists) {
                props.load(fileManager.getInputStream(indextankPath));
            }
        } catch (IOException ioe) {
            throw new IllegalArgumentException(ioe);
        } 


        props.put("indextank.apiUrl", apiUrl);
        props.put("indextank.indexName", indexName);

        OutputStream os = null;

        try {
            MutableFile mf = indextankExists ? fileManager.updateFile(indextankPath) : fileManager.createFile(indextankPath);
            os = mf.getOutputStream();
            props.store(os, "Updated at " + new Date());
        } catch (IOException ioe) { 
            throw new IllegalStateException(ioe);
        }  finally {
            if (os != null) {
                try { os.close(); } catch (IOException ignored) { /* the name says it all */}
            }
        }
    }


    /** {@inheritDoc} */
    public void addAll() {
        Set<ClassOrInterfaceTypeDetails> cids = typeLocationService.findClassesOrInterfaceDetailsWithAnnotation( new JavaType( RooEntity.class.getName() ) );
        for (ClassOrInterfaceTypeDetails cid: cids) {

            // it makes no sense to index abstract classes / interfaces
            if (Modifier.isAbstract(cid.getModifier())) {
                continue;
            }

            addIndextankSearchableAnnotation(cid);            
        }
    }

    
    /** {@inheritDoc} */
    public void addSearch(JavaType javaType) {
        Assert.notNull(javaType, "Java type required");

        String id = physicalTypeMetadataProvider.findIdentifier(javaType);
        Assert.notNull( id, "Cannot locate source for '" + javaType.getFullyQualifiedTypeName() + "'");

        PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService.get(id);
        Assert.notNull(ptm, "Java source code unavailable for type " + PhysicalTypeIdentifier.getFriendlyName(id));

        PhysicalTypeDetails ptd = ptm.getMemberHoldingTypeDetails();
        Assert.notNull(ptd, "Java source code details unavailable for type " + PhysicalTypeIdentifier.getFriendlyName(id));

        Assert.isInstanceOf(MutableClassOrInterfaceTypeDetails.class, ptd, "Java source code is immutable for type " + PhysicalTypeIdentifier.getFriendlyName(id));
        MutableClassOrInterfaceTypeDetails mutableTypeDetails = (MutableClassOrInterfaceTypeDetails) ptd;

        if (Modifier.isAbstract(mutableTypeDetails.getModifier())) {
            throw new IllegalStateException("The class specified is an abstract type. Can only add solr search for concrete types.");
        }

        addIndextankSearchableAnnotation(mutableTypeDetails);
        
    }


    private void addIndextankSearchableAnnotation(ClassOrInterfaceTypeDetails typeDetails){
        JavaType rooIndextankSearchable = new JavaType(RooIndextankSearchable.class.getName());
        if (MemberFindingUtils.getTypeAnnotation(typeDetails, rooIndextankSearchable) == null) {
            AnnotationMetadataBuilder annotationBuilder = new AnnotationMetadataBuilder(rooIndextankSearchable);
            ((MutableClassOrInterfaceTypeDetails) typeDetails).addTypeAnnotation(annotationBuilder.build());
        }
    }

}
